// ==UserScript== // @name 字幕搜索下载预览 (Vue + Element UI) // @namespace http://tampermonkey.net/ // @version 2.2 // @description 使用 Vue 和 Element UI 优化 UI 的下载字幕脚本,增加预览功能 // @author qingtian1 // @include * // @icon https://www.google.com/s2/favicons?sz=64&domain=bbs.tampermonkey.net.cn // @require https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.min.js // @require https://unpkg.com/element-ui@2.14.1/lib/index.js // @require https://cdn.jsdelivr.net/npm/jschardet@3.0.0/dist/jschardet.min.js // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect api-shoulei-ssl.xunlei.com // @connect subtitle.v.geilijiasu.com // ==/UserScript== (function() { 'use strict' // 防止在 iframe 中重复运行代码 if (window.self !== window.top) { console.log('当前页面不是主窗口,无法运行脚本') return // 如果当前不是主窗口,直接退出 } // 动态插入 Element UI 样式 const elementStyle = document.createElement('link') elementStyle.rel = 'stylesheet' elementStyle.href = 'https://unpkg.com/element-ui@2.14.1/lib/theme-chalk/index.css' document.head.appendChild(elementStyle) // 动态插入自定义样式 const customStyle = document.createElement('style') customStyle.type = 'text/css' customStyle.innerHTML = ` .myVueCard { position: fixed; z-index: 9999; left: 0; top: 0; height: 100vh; width: 70vw; background-color: rgba(255, 255, 255, 0.9); transition: all 0.5s; box-shadow: 2px 3px 3px 0 rgba(0, 0, 0, 0.1); } .card__btn { transition: all 0.5s; border-radius: 30px 0 0 30px; width: 30px; height: 60px; background-color: rgb(178, 94, 239); cursor: pointer; position: absolute; right: 0; top: 50%; transform: translateY(-50%); text-align: center; } .card__btn svg { height: 20px; width: 20px; position: absolute; right: 5px; top: 20px; transition: all 0.5s; } .card--hide { left: -70vw; } .card--hide .card__btn { border-radius: 0 30px 30px 0; right: -30px; } .card--hide .card__btn svg { transform: rotate(180deg); } .el-message-box__wrapper { z-index: 10001 !important; } ` document.head.appendChild(customStyle) // 创建 Vue 挂载点 const appContainer = document.createElement('div') appContainer.id = 'subtitleApp' document.body.appendChild(appContainer) // Vue 实例 new Vue({ el: '#subtitleApp', data() { return { isHide: false, // 是否隐藏侧边栏 keyword: '', // 搜索关键字 subtitleList: [], // 字幕列表 loading: false, // 加载状态 showTable: false, // 是否显示表格 showPreview: false, // 是否显示预览对话框 previewContent: '', // 预览的字幕内容 previewLoading: false, // 预览加载状态 isInputFocused: false, // 输入框是否聚焦 isSearchFocused: false, // 输入框是否聚焦 search: '' } }, mounted() { // 添加全局事件监听器,监听输入框的 ESC 键 document.addEventListener('keydown', this.handleKeydown) const subtitleDownloadDefaultIsHide = GM_getValue('subtitleDownloadDefaultIsHide') if (subtitleDownloadDefaultIsHide !== null && subtitleDownloadDefaultIsHide !== undefined) { this.isHide = subtitleDownloadDefaultIsHide } document.addEventListener('selectionchange', () => { const selectedText = window.getSelection().toString() // 实时获取选中的文本 if (selectedText && selectedText.trim() !== '' && !this.isInputFocused) { // console.log('当前选中的文本:', selectedText) this.keyword = selectedText } }) }, methods: { customSort({ column, prop, order }) { let sortIndex = 1 if (order === 'descending') { sortIndex = -1 } if (['分钟', '时间'].includes(column['label'])) { prop = 'duration' this.subtitleList.sort((pre, next) => { const preValue = pre[prop] if (this.isBlank(preValue)) { return 1 } const nextValue = next[prop] if (this.isBlank(nextValue)) { return -1 } const diff = preValue - nextValue return sortIndex * diff }) } else if (['name'].includes(prop)) { this.subtitleList.sort((pre, next) => { const preValue = pre[prop] if (this.isBlank(preValue)) { return 1 } const nextValue = next[prop] if (this.isBlank(nextValue)) { return -1 } const strDiff = preValue.localeCompare(nextValue) return sortIndex * strDiff }) } else { console.log('not fond sort function') } }, isBlank(value) { if (value === undefined || value === '' || value === null || (value instanceof Array && value.length === 0) || (value instanceof Object && Object.keys(value).length === 0) ) { return true } return false }, // 切换侧边栏显示状态 toggleSidebar() { this.isHide = !this.isHide GM_setValue('subtitleDownloadDefaultIsHide', this.isHide) }, // 监听键盘按键 handleKeydown(event) { // console.log('isInputFocused', this.isInputFocused) if (event.keyCode === 27 && this.isInputFocused) { this.keyword = '' this.whenInputClear() } else if (event.keyCode === 27 && this.isSearchFocused) { this.search = '' } }, whenInputClear() { this.showTable = false this.subtitleList.length = 0 }, whenInputChange(value) { if (value.trim() === '') { this.showTable = false this.subtitleList.length = 0 } }, // 搜索字幕 async searchSubtitles() { if (!this.keyword.trim()) { this.$message.error('请输入关键字') return } this.loading = true this.subtitleList = [] const list = await this.requestToXunlei(this.keyword.trim()) if (list.length === 0) { this.$message.warning('未找到相关字幕') } else { this.subtitleList = list this.showTable = true } this.loading = false }, // 请求迅雷 API requestToXunlei(keyword) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api-shoulei-ssl.xunlei.com/oracle/subtitle?name=${keyword}`, // responseType: 'blob', // 确保返回的是 Blob 数据 anonymous: true, // 设置匿名,避免携带 cookies,绕过跨域验证 headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', // 模拟 Chrome 'Cache-Control': 'no-cache', // 请求不要缓存 'Pragma': 'no-cache', // HTTP 1.0 的缓存控制(备用) 'Expires': '0', // 设置过期时间为过去时间,强制刷新 'Referer': 'https://api-shoulei-ssl.xunlei.com/' // 模拟来源页面 }, onload: (response) => { const res = JSON.parse(response.responseText) console.log('res', res) if (res.code === 0 && Array.isArray(res.data)) { res.data.sort((pre, next) => { const preExt = pre['ext'] if (this.isBlank(preExt)) { return 1 } const nextExt = next['ext'] if (this.isBlank(nextExt)) { return -1 } const extDiff = nextExt.localeCompare(preExt) if (extDiff !== 0) { return extDiff } const preDuration = pre['duration'] if (this.isBlank(preDuration)) { return 1 } const nextDuration = next['duration'] if (this.isBlank(nextDuration)) { return -1 } return nextDuration - preDuration }) resolve(res.data) } else { resolve([]) } }, onerror: () => resolve([]) }) }) }, // 下载字幕 downloadSubtitle(row) { const endExt = `.${row.ext}` this.$prompt('请输入文件名,不包含后缀', '保存字幕', { confirmButtonText: '确定', cancelButtonText: '取消', inputValue: row.name.replace(endExt, ''), // 默认文件名 inputValidator: (value) => { return value.trim() !== '' || '文件名不能为空' } }).then(({ value }) => { const filename = value.endsWith(endExt) ? value : `${value}${endExt}` // 自动补充扩展名 console.log(filename) GM_xmlhttpRequest({ method: 'GET', url: row.url, responseType: 'blob', // 确保返回的是 Blob 数据 anonymous: true, // 设置匿名,避免携带 cookies,绕过跨域验证 headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', // 模拟 Chrome 'Cache-Control': 'no-cache', // 请求不要缓存 'Pragma': 'no-cache', // HTTP 1.0 的缓存控制(备用) 'Expires': '0', // 设置过期时间为过去时间,强制刷新 'Referer': 'https://subtitle.v.geilijiasu.com/' // 模拟来源页面 }, onload: (response) => { const blob = response.response const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = filename document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(link.href) this.$set(row, 'isDownloaded', true) // Vue 响应式地更新属性 }, onerror: () => { this.$message.error('下载失败,请重试') } }) }).catch(() => { this.$message.info('取消保存操作') }) }, // 将 ArrayBuffer 转换为 Base64 arrayBufferToBase64(buffer) { let binary = '' const bytes = new Uint8Array(buffer) const length = bytes.byteLength for (let i = 0; i < length; i++) { binary += String.fromCharCode(bytes[i]) } return 'data:application/octet-stream;base64,' + window.btoa(binary) }, // 预览字幕 previewSubtitle(row) { this.previewLoading = true this.showPreview = true this.previewContent = '' // 清空之前的预览内容 GM_xmlhttpRequest({ method: 'GET', url: row.url, responseType: 'arraybuffer', // 确保返回的是 Blob 数据 anonymous: true, // 设置匿名,避免携带 cookies,绕过跨域验证 headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', // 模拟 Chrome 'Cache-Control': 'no-cache', // 请求不要缓存 'Pragma': 'no-cache', // HTTP 1.0 的缓存控制(备用) 'Expires': '0', // 设置过期时间为过去时间,强制刷新 'Referer': 'https://subtitle.v.geilijiasu.com/' // 模拟来源页面 }, onload: (response) => { const arrayBuffer = response.response const base64String = this.arrayBufferToBase64(arrayBuffer) // console.log('Base64 String:', base64String) // 解码 Base64 后进行编码检测 const detected = jschardet.detect(atob(base64String.split(';base64,')[1])) // 从 base64 数据中提取并解码 console.log('Detected encoding:', detected) const encoding = detected.encoding.toLowerCase() const blob = new Blob([arrayBuffer]) // 读取文件内容 const reader = new FileReader() reader.onload = (event) => { const textContent = event.target.result // console.log('Text content:', textContent) this.previewContent = textContent } reader.readAsText(blob, encoding) // 使用检测到的编码读取文件 this.previewLoading = false this.$set(row, 'isPreview', true) // Vue 响应式地更新属性 }, onerror: () => { this.$message.error('加载预览失败') this.previewLoading = false } }) }, // 转换毫秒为分钟 convertMillisecondsToMinutes(milliseconds) { return Math.floor(milliseconds / 1000 / 60) }, // 格式化时间 formatMillisecondsToTime(milliseconds) { const totalSeconds = Math.floor(milliseconds / 1000) const hours = Math.floor(totalSeconds / 3600) const minutes = Math.floor((totalSeconds % 3600) / 60) const seconds = totalSeconds % 60 return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}` } }, template: `
搜索
加载中...
            {{ previewContent }}
          
关闭
` }) })()